Java多线程之如何停止一个线程?

1 如何停止一个线程?

答:(1)不能简单的停止(Stop())一个线程。因为停止stop()会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题

(2)虽然线程不能在中间被停止/干掉,但是任务是可以停止的;想让线程结束的目的是让任务结束,而不是强制线程结束。有两种方式结束任务,分别是:Interrupt和boolean标志位

(3)使用线程中断机制-interrupt停止线程,分2种情况。如果原生支持interrupt:sleep、wait等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个InterruptedException异常,同时清除中断信号,将中断标记位设置成false非原生如果支持interrupt,每执行一次任务,调用interrupted()或isInterrupted()询问一遍系统是否已经中断了,如果中断了系统会告诉我们已经中断了,然后可以实现中断的逻辑

(4)其中,interrupt底层的原理是在Native层加锁。然后判断interrupted_值是否等于true,是的直接返回,表示已经是中断状态了;否则,把interrupted_置为true,发出中断通知。实现原理和boolean标志位的实现逻辑很像
interrupted()与isInterrupted()之间的区别是:在Java层,interrupted()是静态方法,获取当前线程的中断状态后并清空,重复调用后续返回false;isInterrupted()是非静态方法,获取线程对象对应线程的中断状态,不清空,可重复调用,中断清空前一直返回true在Native层,interrupted()比isInterrupted()多调用了SetInterruptedLocked(false)方法,清空当前中断状态,其他的都一致

(5)使用volatile boolean标志位停止线程:线程中设置一个boolean标志位值为false,线程里不断读取这个boolean值,其他地方可以修改这个boolean值;为了保证内存可见性,给boolean标志位添加volatile保证可见性;当某一个线程修改boolean标志位为true,线程中能立刻看到

(6)如何选择interrupt和boolean标志位去停止线程?interrupt()和boolean标志位的原理是一致的。除非是用到了系统方法时(如:sleep) 或者 使用阻塞队列在线程中执行put()时发生阻塞,使用interrupt();否则,建议使用boolean标志位,性能更优,毕竟interrupt使用JNI有一定的开销。。

1.1 这道题想考察什么?

答:(1)考察要点
●是否对线程的用法有了解;是否对线程的stop方法有了解(初级)
●是否对线程stop过程中存在的问题有认识;是否熟悉interrupt中断的用法(中级)
●是否能解释清楚使用boolean标志位的好处;是否知道interrupt底层的细节;通过该题目能够转移话题到线程安全,并阐述无误(高级)

(2)题目剖析
●如何停止一个线程?
●官方停止线程的方法被废弃,所以不能直接简单的停止线程?如何设计可以随时被中断而取消的任务线程?

2 为什么不能简单的停止(Stop())一个线程?

答:因为停止stop()会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题
如下图:Thread1被停止stop()后,立即释放内存锁;Thread3获得内存锁,马上加锁。Thread1根本没有清理内存的机会,原来写数据写到一半,现在没有机会继续写了,因为线程被杀掉了;接着,Thread3获得了时间片,开始读数据时发现内存状态异常,读到一个莫名其妙的数据,因为Thread1还没清理干净就停止线程了,留下一个烂摊子给Thread3,Thread3也会面临crash。所以,这样的停止操作是非常危险的。也正是这个原因,无论什么语言都把停止的api废弃了。

在这里插入图片描述

2.1 为什么暂停和继续(suspend()和resume())也被废弃了?

答:因为对于暂停suspend()和继续resume(),它们的问题在于如果线程调用suspend(),它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题,因为这把锁在线程被resume()之前,是不会被释放的
如下图:Thread1调用暂停suspend()后进入休眠,但是仍持有内存锁;Thread3拿不到锁,会陷入阻塞,等待Thread1持有的内存锁释放。如果Thread1一直持有锁,Thread3一直在等待,就会出现死锁的情况。所以线程suspend()和resume()方法也被废弃了。在这里插入图片描述
在这里插入图片描述

2.2 什么是协作的任务执行模式?

答:虽然线程不能在中间被停止/干掉,但是任务是可以停止的;想让线程结束的目的是让任务结束,而不是强制线程结束

线程在设计过程中,主要是任务执行模式的设计,线程和任务是强绑定的,任务执行完了,线程也就结束了。虽然线程不能在中间被停止/干掉,但是任务是可以停止的,所以任务的执行模式是协作的任务执行模式。我们想让线程结束的目的是让任务结束,而不是强制线程结束。线程被停止和线程自己运行完,对开发者来说是没区别,但是对程序是有明显区别的。线程自己正常运行完,可以正常清理它创造的烂摊子;如果它被干掉了,它的烂摊子就不能被清理了,只能留给其他线程所以,我们在设计上,应该是在任务上添加停止/结束的逻辑,而不是在线程上添加。我们有两种方式结束任务,分别是:Interrupt和boolean标志位

3 如何使用线程中断机制-interrupt停止线程?

答:(1)如果原生支持interrupt:sleep、wait等可以让线程进入阻塞的方法使线程休眠了,而处于休眠中的线程被中断,那么线程是可以感受到中断信号的,并且会抛出一个InterruptedException异常,同时清除中断信号,将中断标记位设置成false。
在这里插入图片描述

// interrupt的原生支持 -> Sleep()
public static void rawInterrupt() throws InterruptedException {
        // 创建目标线程
        Thread interruptRawThread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 线程正在执行...");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println(Thread.currentThread().getName() + " 线程接收到中断信息,中断线程...");
            }
        }, "rawInterrupt");
        // 启动目标线程
        interruptRawThread.start();

        // 中断通知
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("rawInterrupt 设置线程中断....");
        interruptRawThread.interrupt();

//        rawInterrupt 线程正在执行...
//        rawInterrupt 设置线程中断....
//        rawInterrupt 线程接收到中断信息,中断线程...
    }

(2)非原生不支持interrupt
在这里插入图片描述

// 不支持 interrupt()
public static void unInterrupt() {
        // 创建目标线程
        Thread unInterruptThread = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                if (i % 10000 == 0) {
                    System.out.println(Thread.currentThread().getName() + " 线程正在执行... " + i);
                }
            }
        }, "unInterrupt");
        // 启动目标线程
        unInterruptThread.start();

        // 中断通知
        System.out.println("unInterrupt 设置线程中断....");
        unInterruptThread.interrupt();

//        unInterrupt 设置线程中断....
//        unInterrupt 线程正在执行... 0
//        unInterrupt 线程正在执行... 10000
//        unInterrupt 线程正在执行... 20000
//        unInterrupt 线程正在执行... 30000
//        unInterrupt 线程正在执行... 40000
//        unInterrupt 线程正在执行... 50000
//        unInterrupt 线程正在执行... 60000
//        unInterrupt 线程正在执行... 70000
//        unInterrupt 线程正在执行... 80000
//        unInterrupt 线程正在执行... 90000
    }

(3)非原生如果支持interrupt,每执行一次任务,调用interrupted()或isInterrupted()询问一遍系统是否已经中断了,如果中断了系统会告诉我们已经中断了,然后可以实现中断的逻辑

// 支持 interrupted()
public static void supportInterrupted() {
        // 创建目标线程
        Thread supInterruptThread = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                System.out.println(Thread.currentThread().getName() + " 线程正在执行... " + i);
                // 判断当前线程是否中断,
                if (interrupted()) {
                    System.out.println(Thread.currentThread().getName() + " 线程接收到中断信息,中断线程...");
                    break;
                }
            }
        }, "supportInterrupted");
        // 启动目标线程
        supInterruptThread.start();

        // 中断通知
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("supportInterrupted 设置线程中断....");
        supInterruptThread.interrupt();

//        supportInterrupted 设置线程中断....
//        supportInterrupted 线程正在执行... 0
//        supportInterrupted 线程接收到中断信息,中断线程...
    }

// 支持 IsInterrupted()
public static void supportIsInterrupted() {
        // 创建目标线程
        Thread supInterruptThread = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                System.out.println(Thread.currentThread().getName() + " 线程正在执行... " + i + " isInterrupted():" + Thread.currentThread().isInterrupted());
                // 判断该线程是否中断,
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println(Thread.currentThread().getName() + " 线程接收到中断信息,中断线程...");
                    break;
                }
            }
        }, "supportIsInterrupted");
        // 启动目标线程
        supInterruptThread.start();

        // 中断通知
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("supportIsInterrupted 设置线程中断....");
        supInterruptThread.interrupt();

//        supportIsInterrupted 线程正在执行... 0 isInterrupted():false
//        supportIsInterrupted 线程正在执行... 1 isInterrupted():false
//        supportIsInterrupted 线程正在执行... 2 isInterrupted():false
//        supportIsInterrupted 设置线程中断....
//        supportIsInterrupted 线程正在执行... 3 isInterrupted():true
//        supportIsInterrupted 线程接收到中断信息,中断线程...
    }

3.1 是否知道interrupt底层的原理?

答:在Native层加锁。然后判断interrupted_值是否等于true,是的直接返回,表示已经是中断状态了;否则,把interrupted_置为true,发出中断通知。实现原理和boolean标志位的实现逻辑很像

(1)Java层用法

supInterruptThread.interrupt();

(2)Thread.java层的实现

// Thread.java
public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();
    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();  // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
    interrupt0();
}
private native void interrupt0();  // native方法

(3)Native层实现:MutexLock mu(self,*wait_ mutex_)指先加了锁。然后判断interrupted_值是否等于true,是的直接返回,表示已经是中断状态了;否则,把interrupted_置为true,发出中断通知。实现原理和boolean标志位的实现逻辑很像
在这里插入图片描述

补充最新源码:/thread.cc# Interrupt()
在这里插入图片描述

3.2 interrupted()与isInterrupted()有什么区别?

答:在Java层,interrupted()是静态方法,获取当前线程的中断状态后并清空,重复调用后续返回false;isInterrupted()是非静态方法,获取线程对象对应线程的中断状态,不清空,可重复调用,中断清空前一直返回true在Native层,interrupted()比isInterrupted()多调用了SetInterruptedLocked(false)方法,清空当前中断状态,其他的都一致

(1)Java层的区别:
在这里插入图片描述
(2)Native层-Thread.cc的区别:
interrupted()是静态方法:先拿到JNI的环境JNIEnv,他里面有个成员叫self,self就是底层线程的对象,调用的是这个self对象的interrupted()方法
isInterrupted()是非静态方法:对应一个Java线程的对象,先找到这个Java线程的对象对应的底层线程对象,再调用这个底层线程对象的isInterrupted()方法
在这里插入图片描述
补充最新源码:/thread.cc# interrupted() + isInterrupted()
在这里插入图片描述

(3)Native层-thread.cc的区别
最核心区别:interrupted()比isInterrupted()多调用了SetInterruptedLocked(false)方法,清空当前中断状态,其他的都一致
在这里插入图片描述
补充最新源码:/thread.cc# interrupted() + isInterrupted()
在这里插入图片描述
补充最新源码:/thread.cc# storeSequentiallyConsistent()
在这里插入图片描述

3.3 InterruptedException的两种最佳处理方式?

答:在实际开发中肯定是团队协作的,不同的人负责编写不同的方法,然后相互调用来实现整个业务的逻辑。
(1)可以在方法中使用 try/catch 或在方法签名中声明 throws InterruptedException

void subTas() {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        // 在这里不处理该异常是非常不好的
    }
}
void subTask2() throws InterruptedException {
    Thread.sleep(1000);
}

(2)再次中断:如果这时手动添加中断信号,中断信号依然可以被捕捉到。这样后续执行的方法依然可以检测到这里发生过中断,可以做出相应的处理,整个线程可以正常退出。

private void reInterrupt() {
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        e.printStackTrace();
    }
}

4 如何用volatile boolean标志位通知线程停止?

答:(1)线程中设置一个boolean标志位值为false,线程里不断读取这个boolean值,其他地方可以修改这个boolean值;
在这里插入图片描述
(2)但是,boolean标志位没加锁,其他线程修改boolean标志位为true,线程中不一定能看到
在这里插入图片描述
(3)为了保证内存可见性,给boolean标志位添加volatile保证可见性;当某一个线程修改boolean标志位为true,线程中能立刻看到
在这里插入图片描述
(4)案例

// 创建目标线程
static class BooleanThread extends Thread {
        volatile boolean isStopped = false;

        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                System.out.println("volatileBoolean 线程正在执行... " + i);
                // 判断该线程boolean标志位是否有效
                if (isStopped) {
                    System.out.println("volatileBoolean 线程接收到中断信息,中断线程...");
                    break;
                }
            }
        }
    }

// volatile boolean标志位
public static void volatileBoolean() {
        BooleanThread booleanThread = new BooleanThread();
        // 启动目标线程
        booleanThread.start();

        // 中断通知
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("volatileBoolean 设置线程中断....");
        booleanThread.isStopped = true;

//        volatileBoolean 设置线程中断....
//        volatileBoolean 线程正在执行... 0
//        volatileBoolean 线程接收到中断信息,中断线程...
    }

4.1 为什么用boolean标志位的停止方法不一定准确的?

答:使用阻塞队列在线程中执行queue.put()时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断canceled的值的,所以在这种情况下用volatile不能让任务停下来

public static void volatileBooleanNot() {
        try {
            ArrayBlockingQueue storage = new ArrayBlockingQueue(8);
            Producer producer = new Producer(storage);
            Thread producerThread = new Thread(producer);
            producerThread.start();

            Thread.sleep(500);
            Consumer consumer = new Consumer(storage);
            while (consumer.needMoreNums()) {
                System.out.println("volatileBooleanNot " + consumer.storage.take() + "被消费了");
                Thread.sleep(100);
            }
            System.out.println("volatileBooleanNot 消费者不需要更多数据了。");
            // 一旦消费不需要更多数据了,我们应该让生产者也停下来,但是实际情况却停不下来
            producer.canceled = true;
            System.out.println("volatileBooleanNot " + producer.canceled);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 不能结束,不会打印:volatileBooleanNot 生产者结束运行
//        volatileBooleanNot 0是50的倍数,被放到仓库中了。
//        volatileBooleanNot 50是50的倍数,被放到仓库中了。
//        volatileBooleanNot 100是50的倍数,被放到仓库中了。
//        volatileBooleanNot 150是50的倍数,被放到仓库中了。
//        volatileBooleanNot 0被消费了
//        volatileBooleanNot 50被消费了
//        volatileBooleanNot 消费者不需要更多数据了。
//        volatileBooleanNot true
    }

    static class Producer implements Runnable {
        public volatile boolean canceled = false;
        public BlockingQueue storage;

        public Producer(BlockingQueue storage) {
            this.storage = storage;
        }

        @Override
        public void run() {
            int num = 0;
            try {
                while (num <= 100000 && !canceled) {
                    // 生产者在执行storage.put(num)时发生阻塞,在它被叫醒之前是没有办法进入下一次循环判断canceled的值的,所以在这种情况下用volatile不能让生产者停下来。
                    // 相反如果用interrupt语句来中断,即使生产者处于阻塞状态,仍然能够感受到中断信号,并做响应处理。
                    if (num % 50 == 0) {
                        storage.put(num);
                        System.out.println("volatileBooleanNot " + num + "是50的倍数,被放到仓库中了。");
                    }
                    num++;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                System.out.println("volatileBooleanNot 生产者结束运行");
            }
        }
    }

    static class Consumer {
        BlockingQueue storage;
        public Consumer(BlockingQueue storage) {
            this.storage = storage;
        }
        public boolean needMoreNums() {
            if (Math.random() > 0.97) {
                return false;
            }
            return true;
        }
    }

5 如何选择interrupt和boolean标志位去停止线程?

答:interrupt()和boolean标志位的原理是一致的。除非是用到了系统方法时(如:sleep) 或者 使用阻塞队列在线程中执行put()时发生阻塞,使用interrupt();否则,建议使用boolean标志位,性能更优,毕竟interrupt使用JNI有一定的开销
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值